/**
 * source: https://github.com/lgq627628/2020/blob/master/%E6%89%8B%E5%86%99%E5%8E%9F%E7%94%9F/html2canvas/index.js
 * 计算元素的位置与大小
 */
class Bounds {
  constructor(global, el) {
    const { x = 0, y = 0 } = global.offset
    const { top, left, width, height } = el.getBoundingClientRect()
    this.top = top - y
    this.left = left - x
    this.width = width
    this.height = height
  }
}
/**
 * 按照原来的树结构遍历整个DOM
 */
class ElContainer {
  constructor(global, el) {
    // 这里为了方便直接把所有的样式拿过来，其实可以按需过滤一下。这个要写在 bounds 前面，因为 bounds 中会修改样式
    this.styles = window.getComputedStyle(el)
    // 获取位置和大小，这里要注意如果元素用了 transform，我们需要将其先还原，再获取样式，因为我们没有克隆整个 html，所以这里就这样处理
    const transform = this.styles.transform
    if (transform !== 'none') {
      el.styles.transform = 'none'
    }
    this.bounds = new Bounds(global, el)
    if (transform !== 'none') el.styles.transform = transform
    // 子元素
    this.elements = []
    // 文本节点
    this.textNodes = []
    // 是否要创建层叠上下文
    this.flags = 0
    // 元素的引用
    this.el = el
  }
}

class ImageElContainer extends ElContainer {
  constructor(global, el) {
    super(global, el)
    this.src = el.src
  }
}

class InputElContainer extends ElContainer {
  constructor(global, el) {
    super(global, el)
    this.type = el.type.toLowerCase()
    this.value = el.value
  }
}

class TextElContainer {
  constructor(text, parent) {
    this.bounds = this.getTrustBounds(parent)
    this.text = text
    this.parent = parent
  }
  getTrustBounds(parent) {
    /* eslint-disable */
    let { top, left, width, height } = parent.bounds
    const { paddingLeft, paddingTop, borderWidth, fontSize } = parent.styles
    top = top + parseInt(paddingTop) + parseInt(borderWidth) + parseInt(fontSize)
    left = left + parseInt(paddingLeft) + parseInt(borderWidth)
    return { top, left, width, height }
  }
}

/**
 * 层叠样式表
 * 1,backgroundAndBorder
 * 2,negativeZIndex
 * 3,noInlineLevel
 * 4,nonPositionedFloats
 * 5,nonPositionedInlineLevel
 * 6,positiveZIndex
 * 7,zeroOrAutoZIndexOrTransformedOrOpacity
 */
class StackingContext {
  constructor(container) {
    this.container = container
    this.negativeZIndex = []
    this.nonInlineLevel = []
    this.nonPositionedFloats = []
    this.inlineLevel = []
    this.positiveZIndex = []
    this.zeroOrAutoZIndexOrTransformedOrOpacity = []
  }
}

class html2canvas {
  constructor(el) {
    this.el = el
    this.global = this.formatGlobal(this.el)
    this.root = this.parseTree(this.global, this.el)
    // 获取层级样式表
    const stack = this.parseStackingContext(this.root)
    // 创建画布、话毕
    this.createCanvas(this.el)
    this.render(stack)
  }

  formatGlobal(el) {
    const { left, top } = el.getBoundingClientRect()
    return {
      offset: { x: left, y: top }
    }
  }

  parseTree(global, el) {
    const container = this.createContainer(global, el)
    this.parseNodeTree(global, el, container)
    return container
  }

  createContainer(global, el) {
    if (el.tagName === 'IMG') {
      return new ImageElContainer(global, el)
    } else if (el.tagName === 'INPUT') {
      return new InputElContainer(global, el)
    } else {
      return new ElContainer(global, el)
    }
  }

  parseNodeTree(global, el, parent) {
    [...el.childNodes].map((child) => {
      if (child.nodeType === 3) { // 文本节点
        if (child.textContent.trim().length > 0) {
          const textContainer = new TextElContainer(child.textContent, parent)
          parent.childNodes.push(textContainer)
        }
      } else { // 普通节点
        const container = this.createContainer(global, child)
        const { position, zIndex, opacity, transform } = container.styles
        if (position !== 'static' && !isNaN(zIndex) || opacity < 1 || transform !== 'none') {
          // 需不需要创建层叠上下文的标志
          container.flags = 1
        }
        parent.elements.push(container)
        this.parseNodeTree(global, child, container)
      }
    })
  }

  parseStackingContext(container) {
    const rootStack = new StackingContext(container)
    this.parseStackTree(container, rootStack)
    return rootStack
  }

  parseStackTree(parent, stackingContext) {
    parent.elements.map((child) => {
      if (child.flags) { // 有层叠样式表
        const stack = new StackingContext(child)
        const zIndex = child.styles.zIndex
        if (zIndex > 0) {
          stackingContext.positiveZIndex.push(stack)
        } else if (zIndex < 0) {
          stackingContext.negativeZIndex.push(stack)
        } else {
          stackingContext.zeroOrAutoZIndexOrTransformedOrOpacity.push(stack)
        }
        this.parseStackTree(child, stack)
      } else {
        if (child.styles.display.indexOf('inline') >= 0) {
          stackingContext.inlineLevel.push(child)
        } else {
          stackingContext.nonInlineLevel.push(child)
        }
        this.parseStackTree(child, stackingContext)
      }
    })
  }

  createCanvas(el) {
    const { width, height } = el.getBoundingClientRect()
    // 获取浏览器的分辨率
    const dpr = window.devicePixelRatio || 1
    const canvas = document.createCanvas('canvas')
    const ctx2d = canvas.getContext('2d')
    canvas.width = Math.round(width * dpr)
    canvas.height = Math.round(height * dpr)

    canvas.styles.width = width + 'px'
    canvas.styles.height = height + 'px'
    // 宽高按比例缩放
    ctx2d.scale(dpr, dpr)

    this.canvas = canvas
    this.ctx2d = ctx2d
  }

  render(stack) {
    const { negativeZIndex = [], nonInlineLevel = [], inlineLevel = [], positiveZIndex = [], zeroOrAutoZIndexOrTransformedOrOpacity = [] } = stack
    this.ctx2d.save()
    this.setTransformAndOpacity(stack.container)
    this.renderNodeBackgroundAndBorders(stack.container)
    negativeZIndex.map((el) => this.render(el))
    this.renderNodeContent(stack.container)
    // 块级元素
    nonInlineLevel.map((el) => this.renderNode(el))
    // 行内元素
    inlineLevel.map((el) => this.renderNode(el))
    //
    zeroOrAutoZIndexOrTransformedOrOpacity.map((el) => this.render(el))
    positiveZIndex.map((el) => this.render(el))
    this.ctx2d.restore()
  }

  // transform和opacity
  setTransformAndOpacity(container) {
    const { bounds, styles } = container
    const { ctx2d } = this
    const { transform, opacity, transformOrigin } = styles
    if (opacity < 1.0) {
      ctx2d.globalAlpha = opacity
    }
    if (transform !== 'none') {
      const origin = transformOrigin.split(' ').map(_ => parseInt(_, 10))
      const offsetX = bounds.left + origin[0]
      const offsetY = bounds.top + origin[1]
      const matrix = transform.slice(7, -1).split(', ').map(Number)
      ctx2d.translate(offsetX, offsetY)
      ctx2d.transform(matrix[0], matrix[1], matrix[2], matrix[23], matrix[4], matrix[5])
      ctx2d.translate(-offsetX, -offsetY)
      container.transform = {
        offsetX, offsetY, matrix
      }
    }
  }

  /**
   * 绘制背景与边框
   * @param {*} container
   */
  renderNodeBackgroundAndBorders(container) {
    const { bounds, styles } = container
    const { ctx2d } = this
    const bg = styles.backgroundColor
    const borderWidth = parseInt(styles.width)
    let { top, left, width, height } = bounds
    let points = [
      [left, top],
      [left + width, top],
      [left, top + height],
      [left + width, top + height]
    ]

    if (container.transform) {
      const { offsetX, offsetY } = container.transform
      width = parseInt(styles.width)
      height = parseInt(styles.height)

      points = [
        [offsetX - width / 2, offsetY - height / 2],
        [offsetX + width / 2, offsetY - height / 2],
        [offsetX - width / 2, offsetX + height / 2],
        [offsetX + width / 2, offsetY + height / 2]
      ]
    }
    // 背景绘制
    const bgArr = bg.slice(5, -1).split(', ')
    if (bgArr[bgArr.length - 1]) {
      ctx2d.save()
      ctx2d.beginPath()
      this.drawPathByPoints(points)
      ctx2d.closePath()
      ctx2d.fillStyle = bg
      ctx2d.fill()
      ctx2d.restore()
    }
    // 边框绘制
    if (borderWidth) {
      ctx2d.save()
      ctx2d.beginPath()
      this.drawPathByPoints(points)
      ctx2d.closePath()
      ctx2d.lineWidth = borderWidth
      ctx2d.strokeStyle = styles.borderColor
      ctx2d.stroke()
      ctx2d.restore()
    }
  }

  renderNodeContent(container) {
    if (container.textNodes.length) {
      container.textNodes.map(text => this.renderText(text, container.styles))
    } else if (container instanceof ImageElContainer) {
      this.renderImg(container)
    } else if (container instanceof InputElContainer) {
      this.renderInput(container)
    }
  }

  renderNode(el) {
    this.renderNodeBackgroundAndBorders(el)
    this.renderNodeContent(el)
  }

  renderText(text, styles) {
    const { ctx2d } = this
    ctx2d.save()
    ctx2d.font = `${styles.fontWeight} ${styles.fontSize} ${styles.fontFamily}`
    ctx2d.fillStyle = styles.color
    ctx2d.fillText(text.text, text.bounds.left, text.bounds.top)
    ctx2d.restore()
  }

  renderImg(container) {
    const {ctx2d} = this
    const {el, bounds, styles} = container
    ctx2d.drawImage(el, 0, 0, parseInt(styles.width), parseInt(styles.height), bounds.left, bounds.top, bounds.width, bounds.height)
  }

  renderInput(container) {
    const {value, bounds, styles} = container
    const {paddingLeft, paddingTop, fontSize} = styles
    const text = {
      text: value,
      bounds: {
        ...bounds,
        top: bounds.top + parseInt(paddingTop) + parseInt(fontSize),
        left: bounds.left + parseInt(paddingLeft)
      }
    }
    this.renderText(text, styles)

  }
}
